# -*- coding: utf-8 -*-
import base64
import decimal
import functools
import logging
import math
import platform
import random
import re
import string
import threading
import time
from functools import wraps
from pathlib import Path

from .processors import FormatTime

__all__ = [
    'get_logger', 'chunks', 'strptime', 'remove_exponent', 'retry', 'get_size_unit', 'base64_decode', 'random_password',
    'get_dir_path', 'header_to_dict', 'cookie_to_dict', 'form_encode', 'form_decode', 'hump2underline', 'underline2hump', 'try_except',
    'cached_property_ttl', 'lru_cache_ttl'
]

DATEFMT = '%Y-%m-%d %H:%M:%S'


def get_logger(logger, level=logging.INFO):
    if isinstance(logger, str):
        logger = logging.getLogger(logger)
    if not logger.handlers:
        formatter = logging.Formatter(
            f'{platform.node()} %(asctime)s [%(levelname).1s] [%(name)s %(filename)s %(funcName)s %(lineno)d] | %(message)s',
            datefmt=DATEFMT,
        )
        handler = logging.StreamHandler()
        handler.setLevel(level)
        handler.setFormatter(formatter)
    return logger


logger = get_logger(__name__)


def chunks(lst, n):
    for i in range(0, len(lst), n):
        yield lst[i:i + n]


def strptime(value, pattern=None, tzinfo=None):
    return FormatTime(pattern, tzinfo)(value)


def remove_exponent(d, decimal_places=5, rounding=decimal.ROUND_DOWN):
    # https://docs.python.org/3/library/decimal.html#decimal-faq
    if not isinstance(d, decimal.Decimal):
        d = decimal.Decimal(str(d))
    return d.quantize(decimal.Decimal('1')) if d == d.to_integral() else d.quantize(decimal.Decimal('.1') ** decimal_places, rounding=rounding).normalize()


def try_except(*exceptions, message=None, exc_info=False):
    if not exceptions:
        exceptions = (Exception,)

    def decorator(func):
        @wraps(func)
        def _wrapped(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except exceptions as e:
                logger.error(message or e, exc_info=exc_info)

        return _wrapped

    return decorator


def retry(*exceptions, times=3, sleep=0, message=None, exc_info=False):
    if not exceptions:
        exceptions = (Exception,)

    def decorator(func):
        @wraps(func)
        def _wrapped(*args, **kwargs):
            attempt = 1
            while attempt < times:
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    logger.error(message or e, exc_info=exc_info)
                    attempt += 1
                    if sleep:
                        time.sleep(sleep)
            return func(*args, **kwargs)

        return _wrapped

    return decorator


def get_size_unit(size, unit='GB', extra=''):
    """以MB为最小单位换算"""
    if size == 0 or not isinstance(size, (int, float)):
        return size

    unit_list = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'BB', 'NB', 'DB']
    assert unit in unit_list, f'unit must be one of {unit_list}'

    mapping = {}
    step = 1
    for item in unit_list:
        mapping.update({item: step})
        step *= 2 ** 10

    size = size * mapping.get(unit, 2 ** 10)
    index = int(math.floor(decimal.Decimal(math.log(size, 2 ** 10))))
    if index + unit_list.index(unit) > len(unit_list):
        index = len(unit_list) - 1

    size = decimal.Decimal(size / math.pow(1024, index))

    return f'{size:.2f} {unit_list[index]}{extra}'


def base64_decode(base64_str):
    """base64长度自动补全并解码"""
    need_padding = len(base64_str) % 4
    if need_padding:
        missing_padding = 4 - need_padding
        base64_str += '=' * missing_padding

    return base64.urlsafe_b64decode(base64_str).decode('utf-8')


def random_password(length=8, chars=string.ascii_letters + string.digits):
    return ''.join([random.choice(chars) for _ in range(length)])


def get_dir_path(file_dir_path, dirname='.'):
    """获取目录路径

    >>> get_dir_path(__file__, '.')     # cur_dir
    '/Users/wanli/Documents/my_py_tools'
    >>> get_dir_path(__file__, '..')    # parent_dir
    '/Users/wanli/Documents'
    """
    if Path(file_dir_path).is_file():
        file_dir_path = Path(file_dir_path).resolve().parent

    return Path(file_dir_path).joinpath(dirname).resolve()


def header_to_dict(values, default=None):
    """HTTP字符串headers转字典

    >>> HEADERS = '''
            Connection: keep-alive
            X-Anit-Forge-Token:
            Accept-Encoding: gzip, deflate, br
            Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8
        '''
    >>> header_to_dict(HEADERS)
    {'Accept-Encoding': 'gzip, deflate, br',
     'Accept-Language': 'zh-CN,zh;q=0.9,zh-TW;q=0.8',
     'Connection': 'keep-alive',
     'X-Anit-Forge-Token': ''}
    """
    if not isinstance(values, str):
        return default
    headers = {}
    for i in values.split('\n'):
        if i.strip():
            kv = i.split(':', 1)
            headers.update({kv[0].strip(): kv[-1].strip()})
    return headers


def cookie_to_dict(values, default=None):
    """HTTP字符串cookies转字典

    >>> cookies = 'a=111111111;b=222'
    >>> cookie_to_dict(cookies)
    {'a': '111111111', 'b': '222'}
    """
    if not isinstance(values, str):
        return default
    cookies = {}
    for i in values.split(';'):
        if i.strip():
            kv = i.split('=', 1)
            if '|' in kv[-1]:
                cookies.update({kv[0].strip(): {j.split('=', 1)[0].strip(): j.split('=', 1)[-1].strip() for j in
                                                kv[-1].split('|')}})
            else:
                cookies.update({kv[0].strip(): kv[-1].strip()})
    return cookies


def form_encode(values, sort=True):
    """dict转用&拼接的参数

    >>> data = {'name': 'miles', 'age': 1}
    >>> form_encode(data)
    'name=miles&age=1'
    """
    if not isinstance(values, dict):
        return None
    try:
        keys = values.keys()
        if sort:
            keys = sorted(keys)
    except Exception as e:
        logger.warning('{}: {}'.format(values, e))
    else:
        result = '&'.join('{}={}'.format(k, values[k]) for k in keys)
        return result


def form_decode(values, default=None):
    """用&拼接的参数转换成dict

    >>> data = 'name=miles&age=1'
    >>> form_decode(data)
    data = {'name': 'miles', 'age': 1}
    """
    if not isinstance(values, str):
        return default

    try:
        result = {item.split('=')[0]: item.split('=')[1] for item in values.split('&') if item}
    except Exception as e:
        logger.warning(e)
    else:
        return result


def hump2underline(hunp_str: str):
    """驼峰形式字符串转成下划线形式"""
    p = re.compile(r'([a-z]|\d)([A-Z])')
    sub = re.sub(p, r'\1_\2', hunp_str).lower()
    return sub


def underline2hump(underline_str: str):
    """下划线形式字符串转成驼峰形式"""
    return underline_str.title().replace('_', '')


def lru_cache_ttl(ttl: int = 60, maxsize: int = 128, typed: bool = False):
    def decorator(func):
        func = functools.lru_cache(maxsize=maxsize, typed=typed)(func)
        func.delta = ttl * 10 ** 9
        func.expiration = time.monotonic_ns() + func.delta

        @functools.wraps(func)
        def _wrapped(*args, **kwargs):
            if time.monotonic_ns() >= func.expiration:
                func.cache_clear()
                func.expiration = time.monotonic_ns() + func.delta
            return func(*args, **kwargs)

        _wrapped.cache_info = func.cache_info
        _wrapped.cache_clear = func.cache_clear
        return _wrapped

    return decorator


class cached_property_ttl(object):
    def __init__(self, ttl=None):
        if callable(ttl):
            func = ttl
            ttl = None
        else:
            func = None
        self.ttl = ttl
        self._prepare_func(func)
        self.lock = threading.RLock()

    def __call__(self, func):
        self._prepare_func(func)
        return self

    def __get__(self, obj, cls):
        with self.lock:
            if obj is None:
                return self

            now = time.time()
            obj_dict = obj.__dict__
            name = self.__name__
            try:
                value, last_updated = obj_dict[name]
            except KeyError:
                pass
            else:
                ttl_expired = self.ttl and self.ttl < now - last_updated
                if not ttl_expired:
                    return value

            value = self.func(obj)
            obj_dict[name] = (value, now)
            return value

    def __delete__(self, obj):
        obj.__dict__.pop(self.__name__, None)

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = (value, time.time())

    def _prepare_func(self, func):
        self.func = func
        if func:
            self.__doc__ = func.__doc__
            self.__name__ = func.__name__
            self.__module__ = func.__module__
